﻿using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.IO;

namespace ThingyCloud.Network
{
	/// <summary>
	/// The <code>Reader</code> class is responsible for reading the ParleVision server and analyzing the data,
	/// and then transforming it into something we can use.
	/// </summary>
	class ParleVisionReader
	{
		/// <summary>
		/// The IP for the parlevision server
		/// </summary>
		private string ip;

		/// <summary>
		/// Port number of parlevision server
		/// </summary>
		private int port;

		/// <summary>
		/// The client connection to parlevision
		/// </summary>
		private TcpClient client;

		/// <summary>
		/// Indicates whether the thread should stop
		/// </summary>
		private bool stopped = false;

		/// <summary>
		/// An event to register to when a blob has changed
		/// </summary>
		public event EventHandler<Events.BlobChangedEventArgs> BlobChanged;

		/// <summary>
		/// Construct a new instance of a reader and immediately tries to connect to the server.
		/// </summary>
		/// <param name="ip">The IP to connect to</param>
		/// <param name="port">The port to connect to</param>
		public ParleVisionReader(string ip, int port)
		{
			this.ip = ip;
			this.port = port;
		}

		/// <summary>
		/// Tries to connect.
		/// </summary>
		public void Connect()
		{
			// This constructor immediately connects to the remote end
			try
			{
				Debug.WriteLine(string.Format("ParleVisionReader::Connect - Connecting to {0}:{1}...", ip, port));
				client = new TcpClient(ip, port);
			}
			catch (Exception)
			{
				Debug.WriteLine("ParleVisionReader::Connect - Failed to connect! :(");
				//Debug.WriteLine(String.Format("Confirm that ParleVision is running and its TCP server is listening on {0}:{1}", ip, port));
				//Debug.WriteException(e);

				// Failed to connect, wait a while
				Thread.Sleep(2000);
			}
		}

		/// <summary>
		/// Returns whether the TcpClient object is connected to ParleVision
		/// </summary>
		public bool Connected
		{
			get
			{
				return client != null && client.Connected;
			}
		}

		/// <summary>
		/// Disconnect from parlevision
		/// </summary>
		public void Disconnect()
		{
			// Can't disconnect if we're not connected
			if (!Connected)
				return;

			Debug.WriteLine("Disconnecting from ParleVision");
			client.GetStream().Close();
			client.Close();
			client = null;
		}

		/// <summary>
		/// Should be invoked when we have to stop reading from parlevision, will end the thread
		/// </summary>
		public void Stop()
		{
			stopped = true;
		}

		/// <summary>
		/// To be ran as thread, will constantly check the client connection for data.
		/// </summary>
		public void Listen()
		{
			// Check if we're connected
			while (!Connected && !stopped)
			{
				Debug.WriteLine("ParleVisionReader::Listen - Not connected!");
				Connect();
			}

			// Can only do this when we're not connected
			if (Connected)
			{
				// Connected, start listening!
				Debug.WriteLine("ParleVisionReader::Listen - Starting to listen...");

				// Get the stream for data
				NetworkStream stream = client.GetStream();
				Debug.WriteLine(String.Format("ParleVisionReader::Listen - Checking stream.CanRead: {0}", stream.CanRead));

				// Must be able to read
				if (!stream.CanRead)
					return;

				// Prepare buffers
				byte[] buffer_size;
				byte[] buffer_preserved;
				byte[] buffer_protoframe;
				byte[] buffer_framenum;
				byte[] buffer_numargs;
				byte[] buffer_data;

				// Keep reading
				while (Connected && !stopped)
				{
					try
					{
						// A message looks like this:
						// 4 bytes: SIZE
						// 4 bytes: 0
						// 4 bytes: PROTO_FRAME (0x000000)
						// 4 bytes: FRAME_NUMBER
						// 4 bytes: NUMBER_OF_ARGUMENTS
						// and then the data follows

						// Reset all buffers
						buffer_size = new byte[4];
						buffer_preserved = new byte[4];
						buffer_protoframe = new byte[4];
						buffer_framenum = new byte[4];
						buffer_numargs = new byte[4];

						// Read parts into different buffers. Easy.
						stream.Read(buffer_size, 0, 4);
						stream.Read(buffer_preserved, 0, 4);
						stream.Read(buffer_protoframe, 0, 4);
						stream.Read(buffer_framenum, 0, 4);
						stream.Read(buffer_numargs, 0, 4);

						// Reverse arrays if we're on a little endian system
						if (BitConverter.IsLittleEndian)
						{
							Array.Reverse(buffer_size);
							Array.Reverse(buffer_preserved);
							Array.Reverse(buffer_protoframe);
							Array.Reverse(buffer_framenum);
							Array.Reverse(buffer_numargs);
						}

						// And now we can convert this all to uints
						uint size = BitConverter.ToUInt32(buffer_size, 0);
						uint preserved = BitConverter.ToUInt32(buffer_preserved, 0);
						uint proto_frame = BitConverter.ToUInt32(buffer_protoframe, 0);
						uint frame = BitConverter.ToUInt32(buffer_framenum, 0);
						uint numargs = BitConverter.ToUInt32(buffer_numargs, 0);

						// Now the remaining size is actually size - 4 * 4 bytes because of the preserved/proto/frame/numargs
						// buffers we've already read.
						size -= 4 * 4;

						// Create a large enough buffer to fetch the rest
						buffer_data = new byte[size];
						stream.Read(buffer_data, 0, (int)size);
						if (!BitConverter.IsLittleEndian) Array.Reverse(buffer_data);
						string line = Encoding.BigEndianUnicode.GetString(buffer_data);

						// Now process this line
						ProcessParleVisionLine(line);

						// Now we should write that we have received the frame!
						//Debug.WriteLine(string.Format("Sending ACK for frame {0}", proto_frame));
						byte[] ack_header = BitConverter.GetBytes(8);
						byte[] ack_proto = BitConverter.GetBytes(0x000003);
						byte[] ack_framenum = BitConverter.GetBytes(proto_frame);

						if (BitConverter.IsLittleEndian)
						{
							Array.Reverse(ack_header);
							Array.Reverse(ack_proto);
							Array.Reverse(ack_framenum);
						}

						// Now write all ack packets to the stream, and we'll get the next one
						stream.Write(ack_header, 0, 4);
						stream.Write(ack_proto, 0, 4);
						stream.Write(ack_framenum, 0, 4);
						stream.Flush();
					}
					catch (Exception e)
					{
						Debug.WriteLine("Ohnooooo! Someone set us up the bomb! We've lost connection with ParleVision!");
						Debug.WriteCleanLine(e.ToString());
						Disconnect();
						return;
					}
				}
			}

			Debug.WriteLine("ParleVisionReader::Listen - Exited read loop");
		}

		/// <summary>
		/// Process a single line received during a frame from parlevision
		/// </summary>
		/// <param name="line">The line to process</param>
		private void ProcessParleVisionLine(string line)
		{
			string[] parts = line.Split('#');

			// Read all parts
			for (int i = 0; i < parts.Length; i++)
			{
				// Found a track start
				if (parts[i] == "BEGIN_BLOBTRACK")
				{
					string s_id		= parts[++i];
					string s_x		= parts[++i];
					string s_y		= parts[++i];
					string s_width	= parts[++i];
					string s_height	= parts[++i];
						
					string[] parts_id		= s_id.Split(':');
					string[] parts_x		= s_x.Split(':');
					string[] parts_y		= s_y.Split(':');
					string[] parts_width	= s_width.Split(':');
					string[] parts_height	= s_height.Split(':');

					try
					{
						// Convert to uints
						uint id		= Convert.ToUInt32(parts_id[1]);
						uint x		= Convert.ToUInt32(parts_x[1]);
						uint y		= Convert.ToUInt32(parts_y[1]);
						uint width	= Convert.ToUInt32(parts_width[1]);
						uint height	= Convert.ToUInt32(parts_height[1]);

						// Fire the event, KABOOM!
						BlobChanged(
							this,
							new Events.BlobChangedEventArgs()
							{
								ID = id,
								X = x,
								Y = y,
								Width = width,
								Height = height
							}
						);
					}
					catch (Exception e)
					{
						Debug.WriteLine("Caught an exception while processing blob track line");
						Debug.WriteLine("Line parts were:");
						Debug.WriteLine(string.Format("  - {0}", s_id));
						Debug.WriteLine(string.Format("  - {0}", s_x));
						Debug.WriteLine(string.Format("  - {0}", s_y));
						Debug.WriteLine(string.Format("  - {0}", s_width));
						Debug.WriteLine(string.Format("  - {0}", s_height));
						Debug.WriteException(e);
					}
				} // if (begin_blobtrack)
			} // for... reading all parts
		}
	}
}
